Xu hướng mua hàng theo thời điểm trong ngày¶

In [1]:
import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as go
from sklearn.preprocessing import MinMaxScaler
from sklearn.cluster import KMeans

try:
    df = pd.read_csv('OnlineRetail.csv', encoding='latin-1')
except UnicodeDecodeError:
    df = pd.read_csv('OnlineRetail.csv', encoding='windows-1252')
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   InvoiceNo    541909 non-null  object 
 1   StockCode    541909 non-null  object 
 2   Description  540455 non-null  object 
 3   Quantity     541909 non-null  int64  
 4   InvoiceDate  541909 non-null  object 
 5   UnitPrice    541909 non-null  float64
 6   CustomerID   406829 non-null  float64
 7   Country      541909 non-null  object 
dtypes: float64(2), int64(1), object(5)
memory usage: 33.1+ MB
In [2]:
df.head()
Out[2]:
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 1/12/2010 8:26 2.55 17850.0 United Kingdom
1 536365 71053 WHITE METAL LANTERN 6 1/12/2010 8:26 3.39 17850.0 United Kingdom
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 1/12/2010 8:26 2.75 17850.0 United Kingdom
3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 1/12/2010 8:26 3.39 17850.0 United Kingdom
4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 1/12/2010 8:26 3.39 17850.0 United Kingdom

InvoiceNo: Số hóa đơn
StockCode: Mã sản phẩm
Description: Mô tả chi tiết về mặt hàng
Quantity: Số lượng
InvoiceDate: Ngày hóa đơn
UnitPrice: Giá sản phẩm cho mỗi đơn vị hàng hóa tính bằng đơn vị tiền tệ của Anh
CustomerID: Mã khách hàng
Country: Tên quốc gia nơi khách hàng sinh sống hoặc nơi giao dịch được thực hiện

Tiền xử lý dữ liệu¶

In [3]:
df.isna().sum()
Out[3]:
InvoiceNo           0
StockCode           0
Description      1454
Quantity            0
InvoiceDate         0
UnitPrice           0
CustomerID     135080
Country             0
dtype: int64
In [4]:
# Loại các dòng có 'CustomerID' và 'Description' là na
df = df.dropna(subset=['CustomerID'])
df = df.dropna(subset=['Description'])

# Định dạng lại cột InvoiceDate
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'], dayfirst=True, format='mixed')

# Loại bỏ các hàng không thể chuyển đổi
df.dropna(subset=['InvoiceDate'], inplace=True)

# Tách Năm
df['Year'] = df['InvoiceDate'].dt.year

# Tách Quý 
df['Month'] = df['InvoiceDate'].dt.month

# Tách theo buổi (sáng, chiều, tối)
def get_time_shift(hour):
    if 5 <= hour < 12:
        return 'Morning'
    elif 12 <= hour < 18:
        return 'Afternoon'
    else:
        return 'Evening'

df['Time_Shift'] = df['InvoiceDate'].dt.hour.apply(get_time_shift)

# Lọc bỏ giao dịch bị hủy (InvoiceNo bắt đầu bằng ký tự 'C')
df = df[~df['InvoiceNo'].astype(str).str.contains('C')]

# Lọc bỏ Quantity và UnitPrice không hợp lệ (<0)
df = df[(df['Quantity'] > 0) & (df['UnitPrice'] > 0)]

# Tính tổng giá trị đơn hàng
df['TotalPrice'] = df['Quantity'] * df['UnitPrice']

df.head()
Out[4]:
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country Year Month Time_Shift TotalPrice
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom 2010 12 Morning 15.30
1 536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010 12 Morning 20.34
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom 2010 12 Morning 22.00
3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010 12 Morning 20.34
4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010 12 Morning 20.34
In [5]:
df.info()
df.isna().sum()
<class 'pandas.core.frame.DataFrame'>
Index: 397884 entries, 0 to 541908
Data columns (total 12 columns):
 #   Column       Non-Null Count   Dtype         
---  ------       --------------   -----         
 0   InvoiceNo    397884 non-null  object        
 1   StockCode    397884 non-null  object        
 2   Description  397884 non-null  object        
 3   Quantity     397884 non-null  int64         
 4   InvoiceDate  397884 non-null  datetime64[ns]
 5   UnitPrice    397884 non-null  float64       
 6   CustomerID   397884 non-null  float64       
 7   Country      397884 non-null  object        
 8   Year         397884 non-null  int32         
 9   Month        397884 non-null  int32         
 10  Time_Shift   397884 non-null  object        
 11  TotalPrice   397884 non-null  float64       
dtypes: datetime64[ns](1), float64(3), int32(2), int64(1), object(5)
memory usage: 36.4+ MB
Out[5]:
InvoiceNo      0
StockCode      0
Description    0
Quantity       0
InvoiceDate    0
UnitPrice      0
CustomerID     0
Country        0
Year           0
Month          0
Time_Shift     0
TotalPrice     0
dtype: int64

EDA¶

In [6]:
# Giao dịch theo buổi
transaction_counts = df['Time_Shift'].value_counts().reset_index()
transaction_counts.columns = ['Time_Shift', 'Count']

fig_bar = px.bar(transaction_counts,
                 x='Time_Shift',
                 y='Count',
                 color_discrete_sequence=['#491D8B'],
                 title='Số lượng giao dịch theo buổi',
                 hover_data=['Count'],
                 width=900,
                 height=600)
fig_bar.show()

fig_pie = px.pie(transaction_counts,
                 names='Time_Shift',
                 values='Count',
                 color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
                 title='Tỉ lệ giao dịch theo buổi',
                 hover_data=['Count'],
                 width=900,
                 height=600)
fig_pie.show()
In [7]:
# Tổng tiền trung bình theo buổi
avg_spend = df.groupby('Time_Shift')['TotalPrice'].mean().reset_index()
avg_spend.columns = ['Time_Shift', 'AvgPrice']

fig_bar2 = px.bar(
    avg_spend,
    x='Time_Shift',
    y='AvgPrice',
    color_discrete_sequence=['#7D3AC1'],
    title='Tổng tiền trung bình theo buổi',
    hover_data=['AvgPrice'],
    width=900,
    height=600
)
fig_bar2.show()

fig_pie2 = px.pie(
    avg_spend,
    names='Time_Shift',
    values='AvgPrice',
    color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
    title='Tỉ lệ tổng tiền trung bình theo buổi',
    hover_data=['AvgPrice'],
    width=900,
    height=600
)
fig_pie2.show()
In [8]:
# Số khách hàng theo buổi
customer_count = df.groupby('Time_Shift')['CustomerID'].nunique().reset_index()
customer_count.columns = ['Time_Shift', 'CustomerCount']

fig_bar3 = px.bar(
    customer_count,
    x='Time_Shift',
    y='CustomerCount',
    color_discrete_sequence=['#EB548C'],
    title='Số khách hàng theo buổi',
    hover_data=['CustomerCount'],
    width=900,
    height=600
)
fig_bar3.show()

fig_pie3 = px.pie(
    customer_count,
    names='Time_Shift',
    values='CustomerCount',
    color_discrete_sequence=['#491D8B','#7D3AC1','#EB548C'],
    title='Tỉ lệ khách hàng theo buổi',
    hover_data=['CustomerCount'],
    width=900,
    height=600
)
fig_pie3.show()

Chuẩn bị dữ liệu cho phân cụm¶

In [9]:
# Số lượng giao dịch theo buổi
pivot_counts = df.pivot_table(
    index='CustomerID',
    columns='Time_Shift',
    values='InvoiceNo',
    aggfunc='count',
    fill_value=0
)

pivot_counts.columns = [c.lower() + '_count' for c in pivot_counts.columns]
pivot_counts
Out[9]:
afternoon_count evening_count morning_count
CustomerID
12346.0 0 0 1
12347.0 136 0 46
12348.0 3 17 11
12349.0 0 0 73
12350.0 17 0 0
... ... ... ...
18280.0 0 0 10
18281.0 0 0 7
18282.0 7 0 5
18283.0 571 87 98
18287.0 0 0 70

4338 rows × 3 columns

In [10]:
# Trung bình giá trị hóa đơn theo buổi
pivot_avgprice = df.pivot_table(
    index='CustomerID',
    columns='Time_Shift',
    values='TotalPrice',
    aggfunc='mean',
    fill_value=0
)

pivot_avgprice.columns = ['avg_totalprice_' + c.lower() for c in pivot_avgprice.columns]
pivot_avgprice
Out[10]:
avg_totalprice_afternoon avg_totalprice_evening avg_totalprice_morning
CustomerID
12346.0 0.000000 0.000000 77183.600000
12347.0 22.712059 0.000000 26.546957
12348.0 103.333333 52.517647 54.040000
12349.0 0.000000 0.000000 24.076027
12350.0 19.670588 0.000000 0.000000
... ... ... ...
18280.0 0.000000 0.000000 18.060000
18281.0 0.000000 0.000000 11.545714
18282.0 14.315714 0.000000 15.568000
18283.0 2.775797 2.342644 3.123367
18287.0 0.000000 0.000000 26.246857

4338 rows × 3 columns

In [11]:
# Tổng số đơn hàng của khách
total_orders = df.groupby('CustomerID')['InvoiceNo'].nunique().rename('total_orders')

# Tổng chi tiêu của khách
total_spending = df.groupby('CustomerID')['TotalPrice'].sum().rename('total_spending')

customer_features = (
    pivot_counts
    .join(pivot_avgprice)
    .join(total_orders)
    .join(total_spending)
)

customer_features.head()
Out[11]:
afternoon_count evening_count morning_count avg_totalprice_afternoon avg_totalprice_evening avg_totalprice_morning total_orders total_spending
CustomerID
12346.0 0 0 1 0.000000 0.000000 77183.600000 1 77183.60
12347.0 136 0 46 22.712059 0.000000 26.546957 7 4310.00
12348.0 3 17 11 103.333333 52.517647 54.040000 4 1797.24
12349.0 0 0 73 0.000000 0.000000 24.076027 1 1757.55
12350.0 17 0 0 19.670588 0.000000 0.000000 1 334.40

Phân cụm K-Means¶

In [12]:
# Chuẩn hóa dữ liệu
from sklearn.preprocessing import StandardScaler

X = customer_features.copy()

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
In [13]:
sse = []
for i in range(1,11):
    kmeans = KMeans(n_clusters=i , max_iter=300)
    kmeans.fit(X_scaled)
    sse.append(kmeans.inertia_)
fig = px.line(y=sse,template="seaborn",title='Eblow Method')
fig.update_layout(width=800, height=600,
title_font_color="#BF40BF",
xaxis=dict(color="#BF40BF",title="Clusters"),
yaxis=dict(color="#BF40BF",title="SSE"))

Từ k = 1 → 4: SSE giảm mạnh
Từ k = 4 → 5: giảm vừa
Từ k = 5 → 6: gần như không giảm
Từ k = 6 → 9: SSE tiếp tục giảm nhưng rất ít
--> Chọn k = 5

In [14]:
kmeans = KMeans(n_clusters=5, random_state=42)
customer_features['cluster'] = kmeans.fit_predict(X_scaled)
In [15]:
from sklearn.decomposition import PCA

pca = PCA(n_components=2)
principal_components = pca.fit_transform(X_scaled)

customer_features["pca1"] = principal_components[:, 0]
customer_features["pca2"] = principal_components[:, 1]

centroids_reduced = pca.transform(kmeans.cluster_centers_)

cluster_info = [
    {'cluster_id': 0, 'color': '#DB4CB2', 'name': 'Cụm 0'},
    {'cluster_id': 1, 'color': '#c9e9f6', 'name': 'Cụm 1'},
    {'cluster_id': 2, 'color': '#7D3AC1', 'name': 'Cụm 2'},
    {'cluster_id': 3, 'color': '#FFC857', 'name': 'Cụm 3'},
    {'cluster_id': 4, 'color': '#4CAF50', 'name': 'Cụm 4'}
]

# Tạo figure
fig = go.Figure()

# THÊM CÁC CỤM
for info in cluster_info:
    c_id = info['cluster_id']
    c_name = info['name']
    c_color = info['color']

    subset = customer_features[customer_features["cluster"] == c_id]

    fig.add_trace(go.Scatter(
        x=subset['pca1'],
        y=subset['pca2'],
        mode='markers',
        marker=dict(color=c_color, size=7),
        name=c_name,
        hovertemplate=
            "<b>Khách hàng:</b> %{customdata[0]}<br>" +
            "Morning Count: %{customdata[1]}<br>" +
            "Afternoon Count: %{customdata[2]}<br>" +
            "Evening Count: %{customdata[3]}<br>" +
            "Avg Morning: %{customdata[4]:.2f}<br>" +
            "Avg Afternoon: %{customdata[5]:.2f}<br>" +
            "Avg Evening: %{customdata[6]:.2f}<br>" +
            "Total Orders: %{customdata[7]}<br>" +
            "Total Spending: %{customdata[8]:.2f}<br>" +
            "<extra></extra>",
        customdata=subset[
            ["morning_count", "afternoon_count", "evening_count",
             "avg_totalprice_morning", "avg_totalprice_afternoon", "avg_totalprice_evening",
             "total_orders", "total_spending"]
        ].values
    ))

# THÊM TÂM CỤM
fig.add_trace(go.Scatter(
    x=centroids_reduced[:, 0],
    y=centroids_reduced[:, 1],
    mode='markers',
    marker=dict(
        color='white',
        symbol='star',
        size=18,
        line=dict(width=2, color='black')
    ),
    name='Tâm Cụm (Centroids)',
    hoverinfo='skip'
))

# Layout giống bạn
fig.update_layout(
    template='plotly_dark',
    width=1000,
    height=600,
    title='Kết quả phân cụm K-Means khách hàng',
    legend_title="Nhóm khách hàng"
)

fig.show()

Cụm 0

  • Tập trung sát gốc toạ độ ⇒ hoạt động mua hàng thấp và ổn định.
  • Mua hàng rải rác giữa Morning/Afternoon/Evening, nhưng số đơn và chi tiêu thấp.
  • Là nhóm đông nhất → khách phổ thông.

Nhóm khách hàng mua ít, chi tiêu thấp, hành vi ổn định – tệp khách phổ biến nhất.
Cụm 1

  • Nằm ngang rộng theo trục x (có điểm lên tới x = 40–45).
  • Điều này thể hiện tổng chi tiêu hoặc tổng số đơn rất cao.
  • Đây là nhóm "đột biến" so với cụm nền (cụm 0 và 4).
  • Trải rộng ⇒ mức chi tiêu khách trong cụm này không đều.

Nhóm khách hàng chi tiêu cao / nhiều đơn, nhưng phân tán – có khách chi mạnh, có khách mua nhiều đơn.
Cụm 2

  • Cao trên trục y ⇒ hành vi mua hàng đa dạng theo buổi.
  • Có sự cân bằng giữa morning–afternoon–evening.
  • Chi tiêu trung bình, không quá thấp, không quá cao.
  • Nhóm này có hành vi "đa khung giờ".

Nhóm khách mua hàng ở nhiều buổi khác nhau, hoạt động ổn định – tệp khách đa dạng.
Cụm 3

  • Được đẩy xa lên phía trục y ⇒ mua vào các buổi khác lệch hẳn (thường Evening).
  • Số lượng khách rất ít → đặc tính hành vi độc đáo.
  • Có thể chi tiêu trung bình cao hoặc số lần mua đặc biệt nhiều vào một khung giờ.

Nhóm khách có hành vi mua đặc thù (thường mua buổi tối), số đơn không nhiều nhưng chi tiêu khá.
Cụm 4

  • Rất nhỏ gọn, nằm gần cụm 0 nhưng lệch nhẹ về 1 hướng.
  • Dường như tập trung nhiều hơn vào một buổi cố định (Morning hoặc Afternoon).
  • Chi tiêu thấp nhưng có một thói quen mua lặp lại.

Nhóm khách mua đều đặn vào một buổi cố định, nhưng tổng chi tiêu thấp.